mod region;

use chrono::{Datelike, NaiveDate};

#[derive(Debug)]
struct IdCard {
    number: String,
}

impl IdCard {
    fn from(number: &str) -> Option<Self> {
        match Self::validate(number) {
            true => {
                let normalized_number = if number.len() == 15 {
                    Self::upgrade(number)
                } else {
                    number.to_string()
                };
                Some(Self {
                    number: normalized_number,
                })
            }
            false => {
                None
            }
        }
    }

    /// 15 to 18
    fn upgrade(number: &str) -> String {
        let number = number.trim().to_ascii_uppercase();

        if number.len() != 15 || !Self::is_numeric(&number) {
            panic!()
        }

        let birthday = format!("19{}", &number[6..12]);
        let birth_date = NaiveDate::parse_from_str(&birthday, "%Y%m%d").unwrap();

        let mut upgraded_id = String::new();
        upgraded_id.push_str(&number[0..6]);
        upgraded_id.push_str(&birth_date.year().to_string());
        upgraded_id.push_str(&number[8..]);

        let id_digits: Vec<u32> = upgraded_id.chars()
            .map(|ch| ch.to_digit(10).unwrap())
            .collect();

        let weight_sum = Self::calculate_weight_sum(&id_digits);
        let check_code = Self::generate_check_code(weight_sum).unwrap();

        upgraded_id.push_str(check_code);
        upgraded_id
    }

    fn validate(number: &str) -> bool {
        match number.len() {
            15 => Self::validate_legacy(number),
            18 => Self::validate_modern(number),
            _ => false
        }
    }

    /// 15 位 身份证验证
    fn validate_legacy(number: &str) -> bool {
        Self::is_numeric(number) &&
            NaiveDate::parse_from_str(&format!("19{}", &number[6..12]), "%Y%m%d").is_ok()
    }

    /// 18 位 身份证验证
    fn validate_modern(number: &str) -> bool {
        NaiveDate::parse_from_str(&number[6..14], "%Y%m%d").is_ok() &&
            Self::is_numeric(&number[0..17]) &&
            Self::validate_check_code(number)
    }

    fn validate_check_code(number: &str) -> bool {
        let code_17 = &number[0..17];
        let last_code = &number[17..];

        let digits: Vec<u32> = code_17.chars()
            .map(|ch| ch.to_digit(10).unwrap())
            .collect();

        let weight_sum = Self::calculate_weight_sum(&digits);
        Self::generate_check_code(weight_sum)
            .map_or(false, |code| code == last_code.to_uppercase())
    }

    fn birth_date(&self) -> String {
        let year = &self.number[6..10];
        let month = &self.number[10..12];
        let date = &self.number[12..14];
        format!("{}年{}月{}日", year, month, date)
    }

    fn gender(&self) -> &'static str {
        let gender_digit = self.number[16..17].parse::<u8>().unwrap_or(0);
        if gender_digit % 2 != 0 { "男" } else { "女" }
    }

    fn region(&self) -> &'static str {
        use region::REGION;
        match REGION.get(&self.number[0..6]) {
            Some(geo) => geo,
            None => "未知地区"
        }
    }

    fn is_numeric(s: &str) -> bool {
        !s.is_empty() && s.chars().all(char::is_numeric)
    }

    fn calculate_weight_sum(digits: &[u32]) -> u32 {
        const WEIGHTS: [u32; 17] = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
        digits.iter()
            .zip(WEIGHTS.iter())
            .map(|(digit, weight)| digit * weight)
            .sum()
    }

    fn generate_check_code(sum: u32) -> Option<&'static str> {
        match sum % 11 {
            0 => Some("1"),
            1 => Some("0"),
            2 => Some("X"),
            3 => Some("9"),
            4 => Some("8"),
            5 => Some("7"),
            6 => Some("6"),
            7 => Some("5"),
            8 => Some("4"),
            9 => Some("3"),
            10 => Some("2"),
            _ => None
        }
    }
}

pub fn check_id_card(id_card_no: &str) -> String {
    match IdCard::from(id_card_no) {
        Some(identity) => {
            format!(
                "身份证号码正确,{},{},{}",
                identity.gender(),
                identity.birth_date(),
                identity.region()
            )
        }
        _ => "身份证号码错误".to_string()
    }
}

